package modeller.queueingnetworksmodel.structure;

import modeller.databasedesignmodel.relation.Relation;
import modeller.databasedesignmodel.servicedemand.IServiceDemand;
import modeller.databasedesignmodel.transaction.*;
import modeller.queueingnetworksmodel.probabilities.TransactionRoutingMatrix;
import org.jdom2.Attribute;
import org.jdom2.Element;
import org.jdom2.Namespace;

import java.util.HashMap;
import java.util.HashSet;

import static queped.quepedconstants.Strings.*;


/**
 * Created by:  Jason Ye
 * Date:        30/05/2012
 * Time:        20:50
 */
public class Sim implements IQueueingNetworkModelXMLNode {

    private HashSet<Relation> relations;
    private HashSet<Transaction> transactions;
    private TransactionRoutingMatrix qnInstantaneousTransitionMatrix;
    private Relation[] relationArray;
    private String fileName;
    private HashMap<Transaction, IServiceDemand> serviceDemands;
    private boolean hasOpenTransactions = false;

    public Sim(TransactionRoutingMatrix qnInstantaneousTransitionMatrix, HashSet<Transaction> transactions, HashSet<Relation> relations, String fileName, HashMap<Transaction, IServiceDemand> serviceDemands) {

        this.qnInstantaneousTransitionMatrix = qnInstantaneousTransitionMatrix;
        this.transactions = transactions;
        this.relations = relations;
        this.fileName = fileName;
        this.serviceDemands = serviceDemands;
        this.relationArray = qnInstantaneousTransitionMatrix.getMapIndexToRelation();

    }

    @Override
    public Element toXMLRepresentation() {

        Element result = new Element(XML_DOCUMENT_ROOT);
        result.setAttribute(XML_A_ROOT_DISABLESTATISTIC, "false");
        result.setAttribute(XML_A_ROOT_LOGDECIMALSEPARATOR, ".");
        result.setAttribute(XML_A_ROOT_LOGDELIM, ";");
        result.setAttribute(XML_A_ROOT_LOGREPLACE, "0");
        result.setAttribute(XML_A_ROOT_MAXSAMPLES, "1000000");
        result.setAttribute(XML_A_ROOT_NAME, fileName);
        result.setAttribute(XML_A_ROOT_POLLING, "1.0");

        Namespace namespace = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");

        // change schema
        Attribute xsi = new Attribute(
                "noNamespaceSchemaLocation",
                "SIMmodeldefinition.xsd", namespace);
        result.setAttribute(xsi);

        //result.setAttribute("xsi:noNamespaceSchemaLocation","SIMmodeldefinition.xsd");

        // elements

        // userclasses = 1 for each transaction
        for (Transaction t : transactions) {
            result.addContent(generateUserClass(t));
        }

        // nodes - one for each relation, plus one for router and refstation
        for (Relation r : relations) {
            result.addContent(generateNode(r));
        }

        // add source and sink node if necessary
        if (hasOpenTransactions) {
            result.addContent(generateSinkNode());
            result.addContent(generateSourceNode());
        }

        // add performance indices
        result = addPerformanceIndices(result);


        // connections = one attribute for each probability in QNInstantaneousTransitionMatrix
        result = generateConnections(result);

        // preload = specify station populations initially
        result.addContent(generatePreloadElem());
        return result;
    }

    private Element generateSourceNode() {

        SourceNode node = new SourceNode(qnInstantaneousTransitionMatrix, transactions);
        return node.toXMLRepresentation();

    }

    private Element generateSinkNode() {

        Element sinkNode = new Element("node");
        sinkNode.setAttribute("name", "Sink 0");

        Element sinkNodeSection = new Element("section");
        sinkNodeSection.setAttribute("className", "JobSink");

        sinkNode.addContent(sinkNodeSection);

        return sinkNode;
    }

    private Element addPerformanceIndices(Element result) {

        HashMap<Transaction, HashSet<Relation>> blah = generateRelationsInTransactions();

        for (Transaction t : blah.keySet()) {

            HashSet<Relation> rset = blah.get(t);
            String transactionName = t.getTransactionName();

            for (Relation r : rset) {
                for (int i = 0; i < measures.length; i++) {
                    result.addContent(createMeasure(r.getRelationName(), transactionName, measures[i]));
                }
            }

            // add systemwide performance indices
            for (int i = 0; i < systemMeasures.length; i++) {
                result.addContent(createMeasure("", transactionName, systemMeasures[i]));
            }
        }

        return result;
    }


    private HashMap<Transaction, HashSet<Relation>> generateRelationsInTransactions() {

        HashMap<Transaction, HashSet<Relation>> returnMap = new HashMap<Transaction, HashSet<Relation>>();
        // performance indices
        for (Transaction t : transactions) {

            HashSet<Relation> rSet = new HashSet<Relation>();
            rSet = getRelationsAccessedByTransaction(rSet, t);
            returnMap.put(t, rSet);

        }

        return returnMap;
    }

    private HashSet<Relation> getRelationsAccessedByTransaction(HashSet<Relation> rSet, Transaction t) {
        for (IProceduralStatement proceduralStatement : t.getProceduralStatements()) {
            rSet = getRelationsAccessedByTransaction(rSet, proceduralStatement);
        }
        return rSet;
    }


    private HashSet<Relation> getRelationsAccessedByTransaction(HashSet<Relation> rSet, IProceduralStatement proceduralStatement) {

        if (proceduralStatement instanceof Branch) {
            Branch branch = (Branch) proceduralStatement;
            rSet = getRelationsAccessedByTransaction(rSet, branch.getTrueStatement());
            rSet = getRelationsAccessedByTransaction(rSet, branch.getFalseStatement());
        } else if (proceduralStatement instanceof Loop) {
            Loop loop = (Loop) proceduralStatement;
            rSet = getRelationsAccessedByTransaction(rSet, loop.getLoopContents());
        } else {
            SQLQuerySequence seq = (SQLQuerySequence) proceduralStatement;
            for (SQLQuery s : seq.getStatementSequence()) {
                rSet.add(s.getRelation().getFirst());
            }
        }
        return rSet;
    }


    private Element createMeasure(String referenceNodeName, String referenceTransaction, String measureType) {

        Element measure = new Element(XML_E_MEASURE);

        measure.setAttribute(XML_A_MEASURE_ALPHA, Double.toString(0.01));
        measure.setAttribute(XML_A_MEASURE_NAME, formulateMeasureName(referenceNodeName, referenceTransaction, measureType));
        measure.setAttribute(XML_A_MEASURE_NODETYPE, NODETYPE_STATION);
        measure.setAttribute(XML_A_MEASURE_PRECISION, "0.03");
        measure.setAttribute(XML_A_MEASURE_VERBOSE, "false");
        measure.setAttribute(XML_A_MEASURE_CLASS, referenceTransaction);
        measure.setAttribute(XML_A_MEASURE_STATION, referenceNodeName);
        measure.setAttribute(XML_A_MEASURE_TYPE, measureType);

        return measure;
    }

    private String formulateMeasureName(String referenceNodeName, String referenceTransaction, String measure) {

        if (referenceNodeName == null || referenceNodeName.equals("")) {
            return referenceTransaction + "_" + measure;
        }
        return referenceNodeName + "_" + referenceTransaction + "_" + measure;
    }

    private Element generatePreloadElem() {

        Element preload = new Element(XML_E_PRELOAD);

        Element stationPops = new Element(XML_E_STATIONPOPULATIONS);
        stationPops.setAttribute(XML_A_PRELOADSTATION_NAME, "Router");
        for (Transaction t : transactions) {
            stationPops.addContent(generateClassPopulationElem(t));
        }

        preload.addContent(stationPops);
        return preload;
    }

    private Element generateClassPopulationElem(Transaction t) {
        Element classPop = new Element(XML_E_CLASSPOPULATION);

        classPop.setAttribute(XML_A_CLASSPOPULATION_NAME, t.getTransactionName());
        classPop.setAttribute(XML_A_CLASSPOPULATION_POPULATION, Integer.toString(((int) t.getTransactionPrevalence())));

        return classPop;
    }

    private Element generateConnections(Element addToThis) {

        HashMap<Transaction, double[][]> probs = qnInstantaneousTransitionMatrix.getRoutingProbabilities();
        boolean[][] connections = new boolean[relations.size() + 1][relations.size() + 1];

        // populate connections
        for (Transaction t : probs.keySet()) {

            double[][] p = probs.get(t);
            for (int i = 0; i < p.length; i++) {
                for (int j = 0; j < p.length; j++) {
                    if (p[i][j] > 0 && !connections[i][j]) {
                        connections[i][j] = true;
                        if (j == 0 && t.isOpen()) {
                            addToThis.addContent(generateConnectionToSinkElem(i));
                        } else {
                            addToThis.addContent(generateConnectionElem(i, j));
                        }
                    }
                }
            }
        }
        if (hasOpenTransactions) {
            addToThis.addContent(generateConnectionFromSourceElem());
        }

        return addToThis;

    }

    private Element generateConnectionFromSourceElem() {
        //init connection element
        Element connection = new Element(XML_E_CONNECTION);
        //set src and dst to the relation name of the relevant nodes
        connection.setAttribute(XML_A_CONNECTION_SOURCE, "Source 0");
        connection.setAttribute(XML_A_CONNECTION_TARGET, "Router");
        return connection;

    }

    private Element generateConnectionToSinkElem(int srcRelationIndex) {
        // find appropriate relation
        Relation src = relationArray[srcRelationIndex];

        //init connection element
        Element connection = new Element(XML_E_CONNECTION);
        //set src and dst to the relation name of the relevant nodes
        connection.setAttribute(XML_A_CONNECTION_SOURCE, src.getRelationName());
        connection.setAttribute(XML_A_CONNECTION_TARGET, "Sink 0");
        return connection;

    }

    private Element generateConnectionElem(int srcRelationIndex, int dstRelationIndex) {

        // find appropriate relation
        Relation src = relationArray[srcRelationIndex];
        Relation dst = relationArray[dstRelationIndex];

        //init connection element
        Element connection = new Element(XML_E_CONNECTION);
        //set src and dst to the relation name of the relevant nodes
        connection.setAttribute(XML_A_CONNECTION_SOURCE, src.getRelationName());
        connection.setAttribute(XML_A_CONNECTION_TARGET, dst.getRelationName());
        return connection;

    }

    /**
     * constructs a new node for the specified relation and returns its JDOM representation.
     *
     * @param r
     * @return
     */
    private Element generateNode(Relation r) {

        Node node = new Node(r, qnInstantaneousTransitionMatrix, transactions, serviceDemands);

        return node.toXMLRepresentation();

    }

    private Element generateUserClass(Transaction t) {

        Element userclass = new Element(XML_E_CLASS);

        if (!t.isOpen()) {

            userclass.setAttribute(XML_A_CLASS_CUSTOMERS, Integer.toString(((int) t.getTransactionPrevalence())));
            userclass.setAttribute(XML_A_PARAMETER_NAME, t.getTransactionName());
            userclass.setAttribute(XML_A_CLASS_PRIORITY, Integer.toString(0));
            userclass.setAttribute(XML_A_CLASS_REFSOURCE, "Router");
            userclass.setAttribute(XML_A_CLASS_TYPE, "closed");


        } else {

            //open transaction
            hasOpenTransactions = true;
            userclass.setAttribute(XML_A_PARAMETER_NAME, t.getTransactionName());
            userclass.setAttribute(XML_A_CLASS_PRIORITY, Integer.toString(0));
            userclass.setAttribute(XML_A_CLASS_REFSOURCE, "Source 0");
            userclass.setAttribute(XML_A_CLASS_TYPE, "open");
        }

        return userclass;

    }


}
